package moon.core.classdef;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

import moon.core.Model;
import moon.core.MoonFactory;
import moon.core.Name;
import moon.core.genericity.FormalPar;
import moon.traverse.Visitor;

/**
 * Type associated to a class definition.
 * 
 * @author <a href="mailto:rmartico@ubu.es">Ra&uacute;l Marticorena</a>
 * @since JavaMoon-2.0
 */
@SuppressWarnings("serial")
public abstract class ClassType extends Type{
	
	/**
	 * Class.
	 */
	private ClassDef classDef;
	
	/**
	 * Complete.
	 */
	private boolean complete;
	
	/**
	 * Real parameters.
	 */
	protected List<Type> realParameters;
	
	/**
	 * Constructor.
	 * 
	 * @param name name
	 */
	public ClassType(Name name){
		super(name);
		complete = true;
		realParameters = new ArrayList<Type>();
	}
	
	/**
	 * Constructor.
	 * 
	 * @param name name
	 * @param classDef class 
	 * @param realParameters real parameters
	 */
	public ClassType(Name name,ClassDef classDef,List<Type> realParameters){
		this(name,classDef);		
		for (Type type : realParameters){
			this.addRealParameter(type);
		}
	}	
	
	/**
	 * Constructor.
	 * 
	 * @param name name
	 * @param classDef class 
	 */
	public ClassType(Name name, 
			ClassDef classDef){
		this(name);
		this.classDef = classDef;
		realParameters = new ArrayList<Type>();
		complete = true;
	}
	
	/**
	 * Gets the unique name.
	 */
	@Override
	public Name getUniqueName(){

		if (classDef != null){ // known the classDef
			StringBuilder listParameters = new StringBuilder();
			if (this.isParametricType()){ // if it is a parametric type
				// compose type<parameter1,parameter2,...,parameter2>
				listParameters.append("<");
				for (Type type : this.getRealParameters()){				
					listParameters.append(type.getUniqueName() + ",");
				}
				listParameters.deleteCharAt(listParameters.length()-1);
				listParameters.append(">");				
				return classDef.getUniqueName().concat((listParameters.toString()));
			}
			else{
				return classDef.getUniqueName();
			}
		}
		else{
			//if (this.getName()!=null){
				return getName();
			//}
			//else{
			//	return null;
			//}
		}
	}
	
	/**
	 * {@inheritDoc}.
	 * 
	 * @return  {@inheritDoc}
	 */
	@Override
	public Name getName(){

		if (classDef != null){ // known the classDef
			
			if (this.isParametricType()){ // if it is a parametric type
				StringBuilder listParameters = new StringBuilder();
				// compose type<parameter1,parameter2,...,parameter2>
				listParameters.append("<");
				for (Type type : this.getRealParameters()){					
					listParameters.append(type.getName());
					listParameters.append(",");					
				}
				listParameters.deleteCharAt(listParameters.length()-1);
				listParameters.append(">");				
				return classDef.getName().concat((listParameters.toString()));
			}
			else{
				return classDef.getName();
			}
		}
		else{
			return this.getName(); 
		}
	}

	/**
	 * Looks up if the type is directly generated by a class source code.
	 * 
	 * @return true if it has been genereted from a source class file
	 */
	/*
	public boolean hasClassDef(){ return this.classDef == null; }
	*/
	
	/**
	 * Adds a real parameter.
	 * 
	 * @param type type
	 */
	public void addRealParameter(Type type){
		// we can have several times the same type...
		this.realParameters.add(type);		
	}
	
	/**
	 * Removes a real parameter in the type.
	 * 
	 * @param index index
	 * @since JavaMoon-2.0.2
	 */
	public void removeRealPar(int index){
		this.realParameters.remove(index);
	}
	
	/**
	 * Gets the class.
	 * 
	 * @return class
	 */
	public ClassDef getClassDef(){
		return this.classDef;
	}
	
	/**
	 * Looks up if the type is complete.
	 * 
	 * @return true if all the formal parameter have a complete substitution
	 */
	public boolean isComplete() { 
		boolean complete = this.complete;
		for (Type type : realParameters){
			if (type instanceof FormalPar){
				return false;
			}
			if (type instanceof ClassType){
				if ( !((ClassType) type).isComplete()){
					return false;
				}
			}
		}
		return complete;
	}
	
	/**
	 * Gets the real parameters.
	 * 
	 * @return real parameters
	 */
	public List<Type> getRealParameters(){
		return this.realParameters;
	}
	
	/**
	 * Gets the real parameters.
	 * 
	 * @param pos index
	 * @return real parameters
	 */
	public Type getRealParameterByIndex(int pos){		
		return this.getRealParameters().get(pos);
	}
	
	/**
	 * Looks up if the type is parametric.
	 * 
	 * @return true if has real parameters
	 */
	public boolean isParametricType() { 
		return (this.getRealParameters()!=null && !this.getRealParameters().isEmpty()); 
	}	
	
	/**
     * Method from Pattern Design Visitor.
     * By RMS.
     * 
     * @param visitor visitor
     */
    public void accept(Visitor visitor){
    	// Hook method
    	visitor.visit(this);
    }
    
    /**
     * Gets the type code.
     * 
     * @return type code
     */
    public abstract String getTypeCode();
    
    /**
     * 
     * @param object
     * @return
     */
    
    @Override
    public boolean equals(Object object) {
    	if (object == null || !(object instanceof ClassType)) {
    		return false;
    	}
    	return ((ClassType) object).getId()==this.getId();
    }
    
    /**
     * {@inheritDoc}
     * 
     * @return {@inheritDoc}
     */
    public List<FormalPar> getRelatedFormalParameters() {
    	List<FormalPar> formalParameters = new ArrayList<FormalPar>();
    	if (this.isParametricType()){
    		List<Type> realParameters = this.getRealParameters();
    		for (Type realParameter : realParameters) {
    			formalParameters.addAll(realParameter.getRelatedFormalParameters());
    		}    	
    	}
    	return formalParameters;    	
    }
    
    /**
	 * Looks up if the type is not complete and defined by
	 * the formal par. i.e List<E> for E, List<ArrayList<E>> for E
	 * 
	 * @return true if the formal par is a member of the type
	 */
	public boolean isDefinedBy(FormalPar fp) { 
		if (this.isComplete()){
			return false;			
		}
		// in other case
		
		for (Type type : realParameters){
			if (type instanceof FormalPar
					&& type.equals(fp)){
				return true;
			}
			if (type instanceof ClassType){
				if (((ClassType) type).isDefinedBy(fp)){
					return true;
				}
			}
		}
		return false;
	}
	
	/**
	 * {@inheritDoc}.
	 * 
	 * @param type {@inheritDoc}
	 * @param formalPar {@inheritDoc}
	 * @return {@inheritDoc}
	 */
	@Override
	public Type calculateType(Type type, FormalPar formalPar, Model model) {
		//System.err.println("Checking: " + this.getUniqueName().toString());
		if (this.isComplete()){
			//System.err.println("Is complete");
			return this; // complete type (or non parametric)		
		}
		// in other case

		List<Type> listOfNewRealParameters = new ArrayList<Type>();	
		//System.err.println("Real Parameters:"+ this.getRealParameters());
		//System.err.println("Real Parameters2:"+ this.realParameters);
		for (Type typeAux : this.getRealParameters()){
			//System.err.println("With real parameter: " + typeAux.getUniqueName().toString());
			if ( (typeAux instanceof FormalPar
					&& typeAux.equals(formalPar)
				||
				(typeAux instanceof ClassType &&
				((ClassType) typeAux).isDefinedBy(formalPar)))
				){
				
				Type newRealParameter = typeAux.calculateType(type, formalPar, model);
				//System.err.println("New real parameter: " + newRealParameter.getUniqueName().toString());
				listOfNewRealParameters.add(newRealParameter);		
				
			} // if
			else {
				listOfNewRealParameters.add(typeAux);
			}
		} // for
		MoonFactory moonFactory = model.getMoonFactory(); 
		return moonFactory.createParametricType(this.getName(), this.getClassDef(), listOfNewRealParameters);
	}
	
	/**
	 * {@inheritDoc}.
	 * 
	 * @return {@inheritDoc}
	 */
	@Override
	public SortedMap<FormalPar, Type> calculateSubstitutions() {
		SortedMap<FormalPar, Type> substitutions = new TreeMap<FormalPar, Type>(
		new Comparator<FormalPar>() {
			@Override
			public int compare(FormalPar fp1, FormalPar fp2){
				return fp1.getIndex()-fp2.getIndex();
			}
			@Override
			public boolean equals(Object o){
				return false;
			}
		});
		List<FormalPar> formalParameters = this.getClassDef().getFormalPars();
		List<Type> realParameters = this.getRealParameters();
		for (int i = 0; i < formalParameters.size(); i++) {
			substitutions.put(formalParameters.get(i), realParameters.get(i));
		}
		return substitutions;		
	}
	
	
	
	/**
	 * Gets the properties as result of replacing formal parameters with
	 * current types. For example: A<C> if there is an attribute T a; returns C a;
	 * 
	 * @return
	 */
	public List<Property> getCalculatedProperties(Model model) {
		List<Property> properties = new ArrayList<Property>();
		List<AttDec> currentAttDec= this.getClassDef().getAttributes();
		for (AttDec attDec : currentAttDec) {
			if (!attDec.getType().isParametricType()) {				
				Type newAttDecType = this.calculateCompleteTypeWithSubstitutions(attDec.getType(), model);
				attDec = model.getMoonFactory().createCopyFromWithType(attDec, newAttDecType);
			}
			properties.add(attDec);			
		}		
		return properties;
	}
	
	/**
     * Calculates the type from replacing formal parameters with current substitutions
     * in class type.
     * 
     * @param class type with real substitutions
     * @return new type replacing the formal par with the real substitution
     */
    public Type calculateCompleteTypeWithSubstitutions(Type type, Model model) {
    	Type newType = type;
    	SortedMap<FormalPar, Type> substitutions = this.calculateSubstitutions();
    	Set<FormalPar> keys = substitutions.keySet();
    	for (FormalPar fp : keys) {
    		newType = newType.calculateType(substitutions.get(fp), fp, model);
    	}
    	return newType;
    }
} // ClassType


